﻿using System;
using System.IO;
using XAMSDK.Types;
using System.Collections.Generic;

namespace XAMSDK
{
    /// <summary>
    /// An XStream is a Field on a XAMObject that contains raw data. It derives from a Stream so can be used in *most* situations
    /// just like a regular .NET stream. Not all methods are implemented and these are noted below.
    /// </summary>
    public class XStream : Stream, IDisposable
    {
        internal static List<XStream> streamObjects = new List<XStream>();

        bool direction = Constants.INPUT_STREAM;
        XStreamHandle streamh = 0;
        XAMHandle xamHandle = 0;
        XString fieldName = null;

        #region IDisposable
        protected bool disposed = false;
        void IDisposable.Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        // The "disposing" boolean allows derived objects to add an additional step to dispose
        // of any IDisposable objects they own dependent on who is calling it i.e. diectly or via the dtor.
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Cleanup managed objects - none here
            }

            if (!disposed)
            {
                // Cleanup any unmanaged objects
                disposed = true;
            }
        }
        #endregion

        #region Stream
        #region Implemented

        /// <summary>
        /// Can Read operations be performed on the XStream?
        /// </summary>
        public override bool CanRead
        {
            get { return direction == Constants.OUTPUT_STREAM; }
        }

        /// <summary>
        /// Can Seek operations be performed on the XStream?
        /// </summary>
        public override bool CanSeek
        {
            get { return direction == Constants.OUTPUT_STREAM; }
        }

        /// <summary>
        /// Can Write operations be performed on the XStream?
        /// </summary>
        public override bool CanWrite
        {
            get { return direction == Constants.INPUT_STREAM; }
        }

        /// <summary>
        /// Explicitly close the XStream
        /// </summary>
        public override void Close()
        {
            lock (streamObjects)
            {
                if (!disposed)
                {
                    int retval = Imports.XStream_Close(streamh);
                    Helpers.CheckAndThrow(retval, "XStream_Close");
                }

                streamh = 0;
                streamObjects.Remove(this);
            }
        }

        /// <summary>
        /// The Length of the XStream. 
        /// Not applicable for Import or Export XStreams.
        /// </summary>
        public override long Length
        {
            get
            {
                if (direction == Constants.INPUT_STREAM)
                    return Position;
                else
                {
                    if (fieldName == null)
                        return -1;
                    
                    long length;
                    int retval = Imports.XAM_GetFieldLength(xamHandle, fieldName, out length);
                    return length;
                }
            }
        }

        /// <summary>
        /// The current position in the XStream
        /// </summary>
        public override long Position
        {
            get
            {
                return Tell;
            }
            set
            {
                Seek(value, SeekOrigin.Begin);
            }
        }

        /// <summary>
        /// Sets the current position in the XStream
        /// </summary>
        /// <param name="offset">the offset relative to the origin</param>
        /// <param name="origin">the origin that the relative offset applies to</param>
        /// <returns></returns>
        public override long Seek(long offset, SeekOrigin origin)
        {
            if (direction == Constants.INPUT_STREAM)
                throw new NotImplementedException();

            int retval;
            long whence = 0;

            switch (origin)
            {
                case SeekOrigin.Begin: whence = 0;
                    break;

                case SeekOrigin.Current: whence = 1;
                    break;

                case SeekOrigin.End: whence = 2;
                    break;
            }

            retval = Imports.XStream_Seek(streamh, offset, whence);
            Helpers.CheckAndThrow(retval, "XStream_Seek");

            return Position;
        }

        /// <summary>
        /// Write data from the buffer to the XStream
        /// </summary>
        /// <param name="buffer">the buffer holding the data to be written</param>
        /// <param name="offset">the offset in the buffer where the data to be written begins</param>
        /// <param name="count">the number of bytes to write to the XStream</param>
        public override void Write(byte[] buffer, int offset, int count)
        {
            if (direction == Constants.OUTPUT_STREAM)
                throw new NotImplementedException();

            long written;
            int retval;
            byte[] myBuffer = buffer;

            if (buffer.Length < (count + offset))
                count = (buffer.Length - offset);

            if (offset != 0)
            {
                myBuffer = new byte[count];
                Array.Copy(buffer, offset, myBuffer, 0, count);
            }

            do
            {
                retval = Imports.XStream_Write(streamh, myBuffer, count, out written);
                count -= (int) written;
                offset += (int) written;
            } while (retval == 0 && count > 0);

            Helpers.CheckAndThrow(retval, "XStream_Write");
        }

        /// <summary>
        /// Read data from an XStream into a buffer
        /// </summary>
        /// <param name="buffer">the buffer that the data will be read into</param>
        /// <param name="offset">the offset in the buffer where the read data will be written</param>
        /// <param name="count">the number of bytes to *try* and read from the XStream</param>
        /// <returns></returns>
        public override int Read(byte[] buffer, int offset, int count)
        {
            long bytesRead = 0;
            int retval = 0;
            byte[] myBuffer = buffer;

            if (direction == Constants.INPUT_STREAM)
                throw new NotImplementedException();

            if (offset != 0)
            {
                myBuffer = new byte[count];
            }

            retval = Imports.XStream_Read(streamh, myBuffer, count, out bytesRead);

            if (bytesRead > 0 && offset > 0) Array.Copy(myBuffer, 0, buffer, offset, bytesRead);

            if (bytesRead == -1) bytesRead = 0;

            Helpers.CheckAndThrow(retval, "XStream_Read");
            return (int)bytesRead;
        }
        #endregion
        #region Not Implemented
        public override void Flush()
        {
            throw new NotImplementedException();
        }

        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }
        #endregion
        #endregion

        #region Constructors
        /// <summary>
        /// Internal constructor called by the XAMObject factory method.
        /// </summary>
        /// <param name="xh">the handle of the XAMObject that created the XStreamHandle</param>
        /// <param name="name">the name of the XStream field that was created</param>
        /// <param name="sh">the XStreamHandle to be associated with this instance</param>
        /// <param name="mode">the mode that the XStream was opened in</param>
        internal XStream(XAMHandle xh, XString name, XStreamHandle sh, bool mode)
            : this(sh, mode)
        {
            fieldName = name;
            xamHandle = xh;
        }

        /// <summary>
        /// Internal constructor called by the XAMObject factory method.
        /// </summary>
        /// <param name="sh">the XStreamHandle to be associated with this instance</param>
        /// <param name="mode">the mode that the XStream was opened in</param>
        internal XStream(XStreamHandle sh, bool mode)
        {
            lock (streamObjects)
            {
                direction = mode;
                streamh = sh;
                streamObjects.Add(this);
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Abandons the current XStream. Should only be used if graceful close is not possible due to errors.
        /// </summary>
        public void Abandon()
        {
            int retval = Imports.XStream_Abandon(streamh);
            Helpers.CheckAndThrow(retval, "XStream_Abandon");

            streamh = 0;
        }

        /// <summary>
        /// Explicit destructor.
        /// </summary>
        ~XStream()
        {
            Dispose(false);
        }

        /// <summary>
        /// Creates an XStream object from an existing XStrem field 
        /// </summary>
        /// <param name="xh">the handle of the object containing the XStream field</param>
        /// <param name="name">the name of the field</param>
        /// <param name="mode">the mode tha tthe XStream should be opened in</param>
        /// <returns></returns>
        static internal XStream Open(XAMHandle xh, XString name, XString mode)
        {
            XStreamHandle streamh;
            bool direction = Constants.INPUT_STREAM;
            int retval = Imports.XAM_OpenXStream(xh, name, mode, out streamh);
            Helpers.CheckAndThrow(retval, "XAM_OpenStream");

            if (mode == Constants.XSTREAM_MODE_READ_ONLY)
                direction = Constants.OUTPUT_STREAM;

            return new XStream(xh, name, streamh, direction);
        }

        /// <summary>
        /// Internal method to find the XStream object corresponding to a supplied XStreamHandle
        /// </summary>
        /// <param name="h">the XStreamHandle</param>
        /// <returns>the corresponding XStream object</returns>
        internal static XStream Get(XStreamHandle h)
        {
            XStream xs;

            lock(streamObjects)
                xs = streamObjects.Find(delegate(XStream s) { return s.streamh == h; });

            return xs;
        }

        /// <summary>
        /// Asynchronously read data from an XStream.
        /// </summary>
        /// <param name="buffer">the buffer to read the data into</param>
        /// <param name="length">the size of the data to *try* and read</param>
        /// <param name="opID">a user supplied ID for the asynchronous operation</param>
        /// <param name="callback">the callback that is called when the operation completes</param>
        /// <returns></returns>
        public XAsyncObject Read(byte[] buffer, long length, long opID, XAsyncCallback callback)
        {
            XAsyncHandle xah;

            int retval = Imports.XStream_AsyncRead(streamh, buffer, length, opID, callback, out xah);
            Helpers.CheckAndThrow(retval, "XStream_AsyncRead");

            return new XAsyncObject(xah, XAsyncType.XStreamRead);
        }

        /// <summary>
        /// Asynchronously write data to an XStream.
        /// </summary>
        /// <param name="buffer">the buffer containing the data to write</param>
        /// <param name="length">the size of the data to write</param>
        /// <param name="opID">a user supplied ID for the asynchronous operation</param>
        /// <param name="callback">the callback that is called when the operation completes</param>
        /// <returns></returns>
        public XAsyncObject Write(byte[] buffer, long length, long opID, XAsyncCallback callback)
        {
            XAsyncHandle xah;

            int retval = Imports.XStream_AsyncWrite(streamh, buffer, length, opID, callback, out xah);
            Helpers.CheckAndThrow(retval, "XStream_AsyncWrite");

            return new XAsyncObject(xah, XAsyncType.XStreamWrite);
        }

        /// <summary>
        /// Asynchronously close an XStream.
        /// </summary>
        /// <param name="opID">a user supplied ID for the asynchronous operation</param>
        /// <param name="callback">the callback that is called when the operation completes</param>
        /// <returns></returns>
        public XAsyncObject Close(long opID, XAsyncCallback callback)
        {
            XAsyncHandle xah;

            int retval = Imports.XStream_AsyncClose(streamh, opID, callback, out xah);
            Helpers.CheckAndThrow(retval, "XStream_AsyncRead");

            return new XAsyncObject(xah, XAsyncType.XStreamClose);
        }
        #endregion

        #region Properties
        /// <summary>
        /// The current position of the XStream?
        /// </summary>
        public long Tell
        {
            get
            {
                long position;
                int retval = Imports.XStream_Tell(streamh, out position);
                Helpers.CheckAndThrow(retval, "XStream_Tell");
                return position;
            }
        }

        /// <summary>
        /// Is the XStream open?
        /// </summary>
        public bool IsOpen
        {
            get
            {
                return (streamh > 0);
            }
        }
        #endregion
    }

}